package alingrad.engine.src.graphics;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Hashtable;

import javax.imageio.ImageIO;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;

import alingrad.engine.src.collisions.CollisionMaskData;


/**
 * A class that loads textures from files.
 * @author alingrad
 *
 */
public class TextureLoader {
	/**
	 * Table that contains all textures.
	 */
	private HashMap<String, Texture> table = new HashMap<String, Texture>();

	/**
	 * Color model for the TYPE_RGBA images.
	 */
	private ColorModel glAlphaColorModel;

	/**
	 * Color model for the TYPE_RGB images.
	 */
	private ColorModel glColorModel;

	/**
	 * Constructs a texture loader and set the color models.
	 */
	public TextureLoader() {
		glAlphaColorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
                new int[] {8,8,8,8},
                true,
                false,
                ComponentColorModel.TRANSLUCENT,
                DataBuffer.TYPE_BYTE);

		glColorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
		                new int[] {8,8,8,0},
		                false,
		                false,
		                ComponentColorModel.OPAQUE,
		                DataBuffer.TYPE_BYTE);
	}

	/**
	 * Gets a texture from a table or loads it from file.
	 * Creates a collision mask if texture will be pixel-perfect collidable.
	 * @param res location of the file.
	 * @param collidable whether the texture is collidable or not.
	 * @return the texture.
	 * @throws IOException
	 */
	public Texture getOrLoadTexture(String res, boolean collidable) throws IOException {
		Texture texture = table.get(res);
		if (texture != null)
			return texture;

		loadTexture(res, collidable);
		return table.get(res);
	}

	/**
	 * Gets a texture from the table.
	 * @param res the name of the texture.
	 * @return the texture.
	 */
	public Texture getTexture(String res) {
		Texture texture = table.get(res);
		if (texture == null) {
			System.err.print("Texture not yet loaded!");
			System.exit(0);
		}
		return texture;
	}

	/**
	 * Creates a textureID.
	 * @return the textureID.
	 */
	private int createTextureID() {
		IntBuffer tmp = BufferUtils.createIntBuffer(1);
		GL11.glGenTextures(tmp);
		return tmp.get();
	}

	/**
	 * Loads the texture from file.
	 * @param res location of the file.
	 * @param collidable whether or not the texture needs pixel-perfect collision.
	 * @return the texture.
	 * @throws IOException
	 */
	public void loadTexture(String res, boolean collidable) throws IOException {
		if (table.containsKey(res))
			return;

		int textureID = createTextureID();
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID);

		Texture texture = new Texture(GL11.GL_TEXTURE_2D, textureID);

		BufferedImage image = loadImage(res);
		texture.setWidth(image.getWidth());
		texture.setHeight(image.getHeight());

		int srcPixelFormat;
		if (image.getColorModel().hasAlpha())
			srcPixelFormat = GL11.GL_RGBA;
		else
			srcPixelFormat = GL11.GL_RGB;

		ByteBuffer textureBuffer = getImageData(image);

		if (collidable) {
			CollisionMaskData data = createCollisionMaskData(image);
			texture.setData(data);
		}

		texture.setTextureHeight(get2Fold(image.getHeight()));
		texture.setTextureWidth(get2Fold(image.getWidth()));

		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);

		GL11.glTexImage2D(GL11.GL_TEXTURE_2D,
				0,
				GL11.GL_RGBA,
				get2Fold(image.getWidth()),
				get2Fold(image.getHeight()),
				0,
				srcPixelFormat, GL11.GL_UNSIGNED_BYTE,
				textureBuffer);

		table.put(res, texture);
	}

	/**
     * Get the closest greater power of 2 to the fold number
     *
     * @param fold The target number
     * @return The power of 2
     */
    private int get2Fold(int fold) {
        int ret = 2;
        while (ret < fold) {
            ret *= 2;
        }
        return ret;
    }

    /**
     * Gets the data from an image to be used by a texture.
     * @param image the mage to receive the data from.
     * @return the data.
     */
	private ByteBuffer getImageData(BufferedImage image) {
		WritableRaster raster;
		BufferedImage texImage;

		int width = get2Fold(image.getWidth());
		int height = get2Fold(image.getHeight());

		if (image.getColorModel().hasAlpha()) {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 4, null);
			texImage = new BufferedImage(glAlphaColorModel,raster,false,new Hashtable<String, Texture>());
		}else {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 3, null);
            texImage = new BufferedImage(glColorModel,raster,false,new Hashtable<String, Texture>());
		}

		Graphics g = texImage.getGraphics();
		g.setColor(new Color(0f, 0f, 0f, 0f));
		g.fillRect(0, 0, width, height);
		g.drawImage(image, 0, 0, null);

		byte[] data = ((DataBufferByte) texImage.getRaster().getDataBuffer()).getData();

		ByteBuffer buffer = ByteBuffer.allocateDirect(data.length);
        buffer.order(ByteOrder.nativeOrder());
        buffer.put(data, 0, data.length);
        buffer.flip();

        return buffer;
	}

	/**
	 * Loads an image from file.
	 * @param res the file location.
	 * @return the image.
	 * @throws IOException
	 */
	private BufferedImage loadImage(String res) throws IOException {
		URL url = TextureLoader.class.getClassLoader().getResource(res);

        if (url == null) {
            throw new IOException("Cannot find: " + res);
        }

        BufferedImage bufferedImage = ImageIO.read(new BufferedInputStream(getClass().getClassLoader().getResourceAsStream(res)));

        return bufferedImage;
	}

	 /**
	 * Creates a collision mask for a texture.
	 */
	private CollisionMaskData createCollisionMaskData(BufferedImage image) {
		int width = image.getWidth();
		int height = image.getHeight();
		CollisionMaskData data = new CollisionMaskData(width, height);
		boolean[][] mask = new boolean[height][width];
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				mask[y][x] = (image.getRGB(x, y) != 0);
			}
		}
		data.setData(mask);
		return data;
	}
}